﻿using System;

using OST.Shared;
using OST.Shared.SharedTypes;
using OST.Shared.Interfaces;
using OST.Calc.SunPos;
using OST.Calc.Frame;

using mfLogger;

namespace OST.Calc.Tracking
{
    using Extensions;
    using Microsoft.SPOT;

    /// <summary>
    /// contains the necessary method to calculate the <see cref="TrackingDay"/> with motor-targets
    /// </summary>
    public static class TrackingCalculator
    {
        /// <summary>
        /// wrapper-property 4 logging
        /// </summary>
        private static ILoggerAppender Logger
        {
            get
            {
                return mfLogger.mfLogger.Instance;
            }
        }

        public static TrackingDay CalculateDay(DateTime targetDay, FrameConfiguration frameConfig, LocationConfiguration location)
        {
            Logger.Debug("CalculateDay(targetDay=" + targetDay.ToString("d") + ") started");
            Stopwatch watch = new Stopwatch();
            watch.Start();

            //remove clock
            targetDay = new DateTime(targetDay.Year, targetDay.Month, targetDay.Day);

            //ISunPosCalculator sunPosCalculator = new LowAcSunPosCalculator();
            ISunPosCalculator sunPosCalculator = new OSTPSunPosCalculator();
             
            var day = new TrackingDay() { Day = targetDay };

            var motorZero = FrameCalculator.CalcMotorStrokeZero(frameConfig.Arm
                                                                , frameConfig.Leg
                                                                , frameConfig.Body);
            if (motorZero <= 0)
                throw new OSTException("CalculateDay: motorZero is <0 (" + motorZero + ") --> CHECK FRAME-Configuration");

            var dc = FrameCalculator.CalcDegreeCoverage(frameConfig.Arm
                                                                , frameConfig.Leg
                                                                , frameConfig.Body
                                                                , frameConfig.Stroke);

            if (dc.Range <= 0)
                throw new OSTException("CalculateDay: invalid-range (" + dc.Range + ") --> CHECK FRAME-Configuration");

            var dcMinNREL = FrameCalculator.ConvertFrameDegree2NRELDegree(dc.Min);
            var dcMaxNREL = FrameCalculator.ConvertFrameDegree2NRELDegree(dc.Max);

            SunPosition currentSunPos = null;
            SunPosition lastSunPos = null;
            DateTime runningTimestamp = new DateTime(targetDay.Year, targetDay.Month, targetDay.Day, 0, 0, 0);
            DateTime nextDay = day.Day.AddDays(1.0);
            DateTime dcStartTimestamp = targetDay, dcEndTimestamp = targetDay;
            bool sunriseAdded = false;
            bool sunsetAdded = false;
            do
            {
                //loop start actions
                currentSunPos = sunPosCalculator.CalcPos(runningTimestamp, location.Long, location.Lat);

                //check 4 sunrise (lastElevation is negative and currentElevation is positive) and use lastSunPos
                if (lastSunPos != null
                    && lastSunPos.Elevation < 0
                    && currentSunPos.Elevation > 0
                    && !sunriseAdded)
                {
                    //add the motorzero @ the targetDay
                    var beforeSunrisePos = sunPosCalculator.CalcPos(targetDay.AddDays(-2.0), location.Long, location.Lat);
                    day.Action.Add(new TrackingAction()
                    {
                        Sunposition = beforeSunrisePos //drive to sunset-point, after the sun sets
                      ,
                        MotorTarget = motorZero
                    });

                    var sunrisePos = sunPosCalculator.CalcPos(runningTimestamp, location.Long, location.Lat);
                    Logger.Debug("sun rise detected on " + runningTimestamp + " with pos" + sunrisePos);
                    //add initial action, depending on the motoralignment (in prototype: east --> strokeMin=0; west would be maxMotorStroke)
                    day.Action.Add(new TrackingAction()
                    {
                        Sunposition = sunrisePos //drive to sunrise-point, before the sun rises
                      ,
                        MotorTarget = 0
                    });
                    sunriseAdded = true;
                }
                else if (currentSunPos.Elevation > 0 && currentSunPos.Azimut > dcMinNREL
                    && dcStartTimestamp == targetDay)//check for motor START driving timestamp
                {
                    dcStartTimestamp = runningTimestamp;
                }
                else if (currentSunPos.Elevation > 0 && currentSunPos.Azimut > dcMinNREL && currentSunPos.Azimut > dcMaxNREL
                    && dcEndTimestamp == targetDay)//check for motor END driving 
                {
                    dcEndTimestamp = runningTimestamp;
                }
                else if (lastSunPos != null
                    && lastSunPos.Elevation > 0
                    && currentSunPos.Elevation < 0
                    && !sunsetAdded
                    && sunriseAdded
                    && dcStartTimestamp != targetDay
                    && dcEndTimestamp != targetDay)//check 4 sunset (lastElevation is positive and currentElevation is negative) and use currentSunPos
                {

                    var sunsetPos = sunPosCalculator.CalcPos(runningTimestamp.AddHours(3.0), location.Long, location.Lat);
                    Logger.Debug("sun set detected on " + runningTimestamp+ " with pos" + sunsetPos);

                    //inverse to sun set
                    day.Action.Add(new TrackingAction()
                    {
                        Sunposition = sunsetPos //drive to sunset-point, after the sun sets
                      ,
                        MotorTarget = motorZero
                    });
                    sunsetAdded = true;
                }

                //do one-minute-steps
                runningTimestamp = runningTimestamp.AddMinutes(1.0);

                lastSunPos = currentSunPos;

            } while (runningTimestamp <= nextDay);


            if (day.Action.Count != 3)
                throw new OSTException("CalculateDay: actionList init-error 3 items expected, " 
                    + day.Action.Count + " found --> CHECK FRAME-Configuration");

            //calculate motor-driving-window details for one-degree-steps
            var drivingDuration = dcEndTimestamp - dcStartTimestamp;
            Logger.Debug("DrivingDuration=" + drivingDuration);
            //10.000 Tick = 1 ms
            var oneDegreeMotorStepDuration = TimeSpan.FromTicks((long)((Stopwatch.ConvertTimeSpan2ElapsedMS(drivingDuration) / dc.Range) * 10000));
            Logger.Debug("OneDegreeStepDuration=" + oneDegreeMotorStepDuration);

            runningTimestamp = dcStartTimestamp;
            for (int i = 0; i < dc.Range; i++)
            {
                //calc sun position 4 one-degree-step
                currentSunPos = sunPosCalculator.CalcPos(runningTimestamp, location.Long, location.Lat);

                //calc the motor-stroke
                var motorAlpha = FrameCalculator.ConvertSunPosAzimut2FrameDegree(currentSunPos.Azimut);
                var motorStroke = FrameCalculator.CalcSafeMotorStroke(motorAlpha
                                                                    , frameConfig.Arm
                                                                    , frameConfig.Leg
                                                                    , frameConfig.Body
                                                                    , frameConfig.Stroke
                                                                    , frameConfig.MotorAlign);

                //insert a new daily-action for one-degree-step @the last-but-one-index
                day.Action.Insert(day.Action.Count - 1, new TrackingAction()
                {
                    Sunposition = currentSunPos
                  ,
                    MotorTarget = motorStroke
                });
                runningTimestamp = runningTimestamp.Add(oneDegreeMotorStepDuration);
            }

            watch.Stop();

            Logger.Debug("CalculateDay(targetDay=" + targetDay + ") ended, within " + watch.RuntimeInMilliseconds + " ms");
            Logger.Debug("CalculateDay(), released " + Debug.GC(true) + " bytes");
            day.Print2DebugLog();

            return day;
        }
    }
}
